﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Xml.Linq;

namespace FlightToolkit.SimProp
{
    public class Symbols
    {
        #region Public
        public static Symbols Load(string symbolPath)
        {
            if (String.IsNullOrEmpty(symbolPath))
            {
                symbolPath = Symbols.DefaultSymbolPath;
            }

            lock (Symbols.symbolSets)
            {
                if (Symbols.symbolSets.ContainsKey(symbolPath))
                {
                    return Symbols.symbolSets[symbolPath];
                }

                var symbols = new Symbols(symbolPath);
                Symbols.symbolSets.Add(symbolPath, symbols);
                return symbols;
            }
        }
        #endregion

        #region Internal
        internal SymbolDef GetSymbolDef(Guid guid)
        {
            foreach (var entry in this.nameToNamespaceDefs)
            {
                var symbolDef = entry.Value.GetLocalSymbolDef(guid);
                if (symbolDef != null)
                {
                    return symbolDef;
                }
            }

            return null;
        }

        internal NamespaceDef GetNamespaceDef(string name)
        {
            return this.nameToNamespaceDefs[name];
        }

        internal void GetNamespaceAndName(string name, out NamespaceDef newNamespace, out string newName)
        {
            var strings = name.Split('.');
            if (strings.Count() == 2)
            {
                newNamespace = GetNamespaceDef(strings[0]);
                newName = strings[1];
            }
            else
            {
                throw new Exception("Expected qualified SimProp symbol name including namespace");
            }
        }

        internal TypeDef LookupTypeDefByName(string name)
        {
            var result = this.typeDefs.Where(x => x.Value.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

            if (result.Count() > 0)
            {
                return result.First().Value;
            }

            return null;
        }
        #endregion

        #region Private
        private static readonly Dictionary<string, Symbols> symbolSets = new Dictionary<string, Symbols>();
        private Dictionary<Guid, TypeDef> typeDefs;
        private Dictionary<string, NamespaceDef> nameToNamespaceDefs;
        internal ModuleBuilder moduleBuilder;

        private delegate Stream OpenSymbolFile(string filename);

        private static string DefaultSymbolPath
        {
            get
            {
                string codeBase = Assembly.GetExecutingAssembly().CodeBase;
                UriBuilder uri = new UriBuilder(codeBase);
                string path = Uri.UnescapeDataString(uri.Path);
                string flightSymbols = Path.Combine(Path.GetDirectoryName(path), "SimPropSymbols\\Flight");
                string toolkitSymbols = Path.Combine(Path.GetDirectoryName(path), "SimPropSymbols\\Toolkit");
                return flightSymbols + ";" + toolkitSymbols;
            }
        }

        private Symbols(string symbolPath)
        {
            // Create a runtime assembly to store enum types generated from teh schema
            AppDomain currentDomain = AppDomain.CurrentDomain;
            AssemblyName assemblyName = new AssemblyName("FlightToolkit.SimProp.Types");
            AssemblyBuilder assemblyBuilder = currentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            this.moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name);


            this.typeDefs = new Dictionary<Guid, TypeDef>();
            this.nameToNamespaceDefs = new Dictionary<string, NamespaceDef>(StringComparer.OrdinalIgnoreCase);

            var filenameToNamespaceDef = new Dictionary<string, NamespaceDef>(StringComparer.OrdinalIgnoreCase);

            var symbolPaths = symbolPath.Split(';');
            var symbolFiles = symbolPaths.SelectMany(x => Directory.GetFiles(x, "*.xml"));

            OpenSymbolFile symbolIncludeFunctor = (string filename) =>
            {
                foreach (var path in symbolPaths)
                {
                    string fullpath = Path.Combine(path, filename);
                    if (File.Exists(fullpath))
                    {
                        return new FileStream(fullpath, FileMode.Open, FileAccess.Read);
                    }
                }
                throw new FileNotFoundException("Could not find symbol file in search paths", filename);
            };

            foreach (var symbolFile in symbolFiles)
            {
                string filename = Path.GetFileName(symbolFile);
                if (!filenameToNamespaceDef.ContainsKey(filename))
                {
                    try
                    {
                        LoadSymbolsFromFile(filenameToNamespaceDef, filename, symbolIncludeFunctor);
                    }
                    catch (Exception e)
                    {
                        throw new Exception("Failed to load symbol file '" + symbolFile + "'", e);
                    }
                }
            }
        }

        private void LoadSymbolsFromFile(Dictionary<string, NamespaceDef> filenameToNamespaceDef, string filename, OpenSymbolFile symbolIncludeFunctor)
        {
            XDocument doc = XDocument.Load(symbolIncludeFunctor(filename));

            var types = doc.Descendants("TypeDef");
            foreach (var typeDef in types)
            {
                var currentTypeDef = new TypeDef(typeDef);

                typeDefs.Add(currentTypeDef.Id, currentTypeDef);
            }

            List<string> includeFiles;
            NamespaceDef currentNamespaceDef = new NamespaceDef(this, doc.Root, out includeFiles);

            var symbolIncludes = doc.Descendants("SymbolInclude");
            foreach (var include in includeFiles)
            {
                if (!filenameToNamespaceDef.ContainsKey(include))
                {
                    try
                    {
                        LoadSymbolsFromFile(filenameToNamespaceDef, include, symbolIncludeFunctor);
                    }
                    catch (Exception e)
                    {
                        throw new Exception("Failed to load symbol file '" + include + "'", e);
                    }
                }
            }

            currentNamespaceDef.Validate(doc.Root, includeFiles.Select(x => filenameToNamespaceDef[x]).ToList());

            this.nameToNamespaceDefs.Add(currentNamespaceDef.Name, currentNamespaceDef);
            filenameToNamespaceDef.Add(filename, currentNamespaceDef);
        }
        #endregion
    }
}
